#pragma once

#include "base.h"
#include "detail/s4.h"
#include "../container/robuf.h"
#include "../updater/s2.h"
#include "../impl/ma.h"
#include "../impl/m4.h"

namespace mdtx::online::indicator
{
    /**
     * @brief Non-parametric rolling skewness
     *
     * @tparam T scala type
     */
    template <typename T>
    class skew_np : public indi_base<T, T, 1, 3>
    {
        using base = indi_base<T, T, 1, 3>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::sorted_robuf<T> buf;
        updater::var<T> var{1};

    public:
        explicit skew_np(ptrdiff_t period)
            : buf(period)
        {
        }

        /**
         * @brief update
         *
         * @param in x
         * @return tuple_out mean, standard deviation, skewness
         */
        tuple_out update(tuple_in const &in) override
        {
            var.insert(in);
            if (buf.buf.full())
            {
                var.remove_impl(buf.buf.front());
            }
            buf.insert(std::get<0>(in));

            T skew{};
            auto [m, sd] = var.get();
            if (sd > 0)
            {
                sd = std::sqrt(sd);
                auto med = buf.sorted[buf.sorted.size() / 2];
                skew = (m - med) / sd;
            }

            return tuple_out(m, sd, skew);
        }
    };

    /**
     * @brief Non-parametric rolling weighted skewness
     *
     * @tparam T scala type
     */
    template <typename T>
    class skew_npw : public indi_base<T, T, 2, 3>
    {
        using base = indi_base<T, T, 2, 3>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::wsorted_robuf<T, T> buf;
        impl::sum<T> wsum{};
        updater::var_vw<T> var{};

    public:
        explicit skew_npw(ptrdiff_t period)
            : buf(period)
        {
        }

        /**
         * @brief update
         *
         * @param in x, weight
         * @return tuple_out mean, standard deviation, skewness
         */
        tuple_out update(tuple_in const &in) override
        {
            auto [x, w] = in;
            var.insert(in);
            if (buf.buf.full())
            {
                const auto &old = buf.buf.front();
                var.remove_impl(old.v, old.w);
                wsum.roll_impl(w, old.w);
            }
            else
            {
                wsum.insert_impl(w);
            }
            buf.insert(x, w);

            T skew{};
            auto [m, sd] = var.get();
            if (sd > 0)
            {
                sd = std::sqrt(sd);
                T w_acc{}, w_half{wsum.s / 2}, med;
                for (auto [val, weight] : buf.sorted)
                {
                    w_acc += weight;
                    if (w_acc > w_half)
                    {
                        med = val;
                        break;
                    }
                }
                skew = (m - med) / sd;
            }

            return tuple_out(m, sd, skew);
        }
    };

    /**
     * @brief Quantile based rolling skewness
     *
     * @tparam T scala type
     */
    template <typename T>
    class skew_quantile : public indi_base<T, T, 1, 3>
    {
        using base = indi_base<T, T, 2, 3>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        container::quantile_robuf<T, 3> buf;

    public:
        explicit skew_quantile(ptrdiff_t period)
            : buf(period, std::array<T, 3>{0.25, 0.5, 0.75})
        {
        }

        /**
         * @brief update
         *
         * @param in x
         * @return tuple_out median, iqr, skewness
         */
        tuple_out update(tuple_in const &in) override
        {
            buf.insert(std::get<0>(in));

            T skew{};
            auto [q1, q2, q3] = buf.get();

            if (buf.buf.full())
            {
                skew = ((q3 - q2) + (q1 - q2)) / (q3 - q1);
            }
            else
            {
                q2 = buf.sorted[buf.sorted.size() / 2];
            }

            return tuple_out(q2, q3 - q1, skew);
        }
    };

    /**
     * @brief Rolling skewness G1 coefficient
     *
     * @tparam T scala type
     */
    template <typename T>
    class skew_G1 : public indi_base<T, T, 1, 3>
    {
        using base = indi_base<T, T, 1, 3>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        boost::circular_buffer<T> buf;
        impl::m4<T> moment{};
        T co_sd{}, co_sk{};

    public:
        explicit skew_G1(ptrdiff_t period)
            : buf(period * 4)
        {
        }

        /**
         * @brief update
         *
         * @param in x
         * @return tuple_out mean, standard deviation, skewness
         */
        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);
            T x2 = x * x;
            T x3 = x2 * x;
            T x4 = x2 * x2;

            if (buf.full())
            {
                moment.roll_impl(x, x2, x3, x4, buf[0], buf[1], buf[2], buf[3]);
            }
            else
            {
                moment.insert_impl(x, x2, x3, x4);
                if (moment.n > 1)
                {
                    co_sd = detail::coefs_sd(moment.n);
                    if (moment.n > 2)
                    {
                        co_sk = detail::coefs_sk(moment.n);
                    }
                }
            }
            bpush(buf, x, x2, x3, x4);

            T sd{}, skew{};
            auto [m, m2, m3, m4] = moment.center_impl();
            if (m2 > 0)
            {
                sd = co_sd * std::sqrt(m2);
                T g1 = m3 / std::pow(m2, 1.5);
                skew = co_sk * g1;
            }

            return tuple_out(m, sd, skew);
        }
    };

    /**
     * @brief Cumulative skewness, G1 coefficient
     *
     * @tparam T scala type
     */
    template <typename T>
    class skew_G1cu : public indi_base<T, T, 1, 3>
    {
        using base = indi_base<T, T, 1, 3>;

    public:
        using typename base::tuple_in;
        using typename base::tuple_out;
        using base::update;

    protected:
        impl::m4<T> moment;
        T co_sd, co_sk;

    public:
        /**
         * @brief update
         *
         * @param in x
         * @return tuple_out mean, standard deviation, skewness
         */
        tuple_out update(tuple_in const &in) override
        {
            T x = std::get<0>(in);
            T x2 = x * x;
            T x3 = x2 * x;
            T x4 = x2 * x2;

            moment.insert_impl(x, x2, x3, x4);
            if (moment.n > 1)
            {
                co_sd = detail::coefs_sd(moment.n);
                if (moment.n > 2)
                {
                    co_sk = detail::coefs_sk(moment.n);
                }
            }

            T sd{}, skew{};
            auto [m, m2, m3, m4] = moment.center_impl();
            if (m2 > 0)
            {
                sd = co_sd * std::sqrt(m2);
                T g1 = m3 / std::pow(m2, 1.5);
                skew = co_sk * g1;
            }

            return tuple_out(m, sd, skew);
        }
    };
}
